package com.github.aesteve.vertx.nubes.utils.async;

import com.github.aesteve.vertx.nubes.utils.functional.TriConsumer;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;

import static com.github.aesteve.vertx.nubes.utils.async.AsyncUtils.completeOrFail;

public class MultipleFutures<T> extends SimpleFuture<T> {

  private final Map<Handler<Future<T>>, Future<T>> consumers;
  private static final Logger LOG = LoggerFactory.getLogger(MultipleFutures.class);

  public MultipleFutures() {
    consumers = new HashMap<>();
  }

  public MultipleFutures(Future<T> after) {
    this();
    join(after);
  }

  public MultipleFutures(Handler<AsyncResult<T>> after) {
    this();
    join(after);
  }

  public <L> MultipleFutures(Collection<L> elements, Function<L, Handler<Future<T>>> transform) {
    this();
    addAll(elements, transform);
  }

  public <L> MultipleFutures(Collection<L> elements, BiConsumer<L, Future<T>> transform) {
    this();
    addAll(elements, transform);
  }

  public <K, V> MultipleFutures(Map<K, V> elements, TriConsumer<K, V, Future<T>> transform) {
    this();
    addAll(elements, transform);
  }

  //

  public MultipleFutures<T> add(Handler<Future<T>> handler) {
    if (handler == null) {
      return this;
    }
    Future<T> future = Future.future();
    future.setHandler(futureHandler -> checkCallHandler());
    consumers.put(handler, future);
    return this;
  }

  public MultipleFutures<T> start() {
    if (consumers.isEmpty()) {
      complete();
      return this;
    }
    consumers.forEach(Handler::handle);
    return this;
  }

  @Override
  public T result() {
    return null;
  }

  @Override
  public Throwable cause() {
    Exception e = new Exception("At least one future failed");
    consumers.forEach((consumer, future) -> {
      if (future.cause() != null) {
        LOG.error(future.cause());
        if (e.getCause() == null) {
          e.initCause(future.cause());
        } else {
          e.addSuppressed(future.cause());
        }
      }
    });
    return e;
  }

  @Override
  public boolean succeeded() {
    return consumers.values().stream().allMatch(Future::succeeded);
  }

  @Override
  public boolean failed() {
    return consumers.values().stream().anyMatch(Future::failed);
  }

  @Override
  public boolean isComplete() {
    if (super.isComplete()) { // has been marked explicitly
      return true;
    }
    return !consumers.isEmpty() &&
        consumers.values().stream().allMatch(Future::isComplete);
  }

  public MultipleFutures<T> join(Future<T> future) {
    setHandler(completeOrFail(future));
    return this;
  }

  public MultipleFutures<T> join(Handler<AsyncResult<T>> handler) {
    setHandler(handler);
    return this;
  }

  public <L> MultipleFutures<T> addAll(Collection<L> list, BiConsumer<L, Future<T>> transform) {
    list.forEach(element -> add(res -> transform.accept(element, res)));
    return this;
  }

  public <L> MultipleFutures<T> addAll(Collection<L> list, Function<L, Handler<Future<T>>> transform) {
    list.forEach(element -> add(transform.apply(element)));
    return this;
  }

  public <L> MultipleFutures<T> treatAllNow(Collection<L> list, Function<L, Handler<Future<T>>> transform) {
    addAll(list, transform);
    start();
    return this;
  }

  public <K, V> MultipleFutures<T> addAll(Map<K, V> map, TriConsumer<K, V, Future<T>> transform) {
    map.forEach((key, value) -> add(res -> transform.accept(key, value, res)));
    return this;
  }

}
